Skip to content

feat(devtools): feature flag control from the Flags tab - #36

Open
mono424 wants to merge 1 commit into
mainfrom
feat/devtools-feature-flags
Open

feat(devtools): feature flag control from the Flags tab#36
mono424 wants to merge 1 commit into
mainfrom
feat/devtools-feature-flags

Conversation

@mono424

@mono424 mono424 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Why

Feature flags could only be changed with spky flag from a terminal holding root credentials. A developer running the app in a browser had no way to flip a flag for themselves, and no way to roll one out to other users without dropping to the CLI.

What

A Flags tab in the DevTools extension, with two capabilities kept deliberately apart because their blast radii differ:

Scope Auth
Local overrides This browser only None — works signed out and offline
Remote changes Every user Admin

Remote changes cover the global enabled bit and per-user allowlist membership. Creating, deleting and percentage rollouts stay with spky flag.

The admin model

sp00ky had no admin concept, only record tokens and root. The user table belongs to the app, so gating on a role field would require every app to adopt one and hand users a field they could write to promote themselves.

Instead: _00_admin, a new internal table written only by root via spky admin add|remove|list. It denies create/update/delete unconditionally, and its select rule is self-scoped — an admin can confirm their own status without the roster being enumerable.

Why a materialize function was needed

_00_user_feature is already live-synced, but it is derived. Writing a definition syncs nothing on its own, and the scheduler sweep skips (user, flag) pairs that already have a row. fn::feature::materialize re-evaluates one flag for all users; allow/disallow edit the allowlist and re-materialize.

Three SurrealDB behaviours that shaped this

Verified against surrealdb-core-3.1.5 rather than assumed:

  • Permission-clause subqueries run with checks disabled, so _00_admin can gate other tables while staying invisible itself.
  • DEFINE FUNCTION defaults to PERMISSIONS FULL. Omitting the clause does not make a function root-only — it ships a self-service flag editor to every signed-in user. All three mutations carry an explicit clause, and a unit test asserts it.
  • Custom functions are not security-definer; the body runs as the caller. Hence _00_user_feature opening writes to admins, and hence the SELECT id FROM user inside materialize being permission-filtered — the scheduler now stamps the root-enumerated user count so materialize throws loudly instead of silently updating only the admin.

Drive-by fix

fn::feature::hash was broken on SurrealDB 3: the int cast no longer accepts hex and hex literals were removed from the language, so <int>'0x5f7bdee4' errors. Nothing called it before (both Rust evaluators hash themselves), but every rollout rule would have failed the moment materialize did. Replaced with a hand-rolled hex fold that reproduces the pinned Rust golden vectors exactly.

Concurrency

allow/disallow are read-modify-write over the rules array, matching spky flag. Concurrent admins collide — but SurrealDB fails the loser with a retryable transaction conflict rather than dropping the write (verified with 4 parallel calls), so the bridge retries with jitter instead of surfacing a raw engine error.

Tests

  • New Docker-backed e2e (feature_materialize_e2e.rs, #[ignore], pinned to SurrealDB 3.x): asserts the SurrealQL evaluator matches the Rust golden vectors, and the full permission matrix — a non-admin cannot read definitions, forge a _00_user_feature row, or join the roster.
  • 10 new unit tests for local overrides.
  • The Playwright permission spec gains admin coverage.
  • Core 481/481, scheduler 90/90, CLI 393 passing.

Notes for the reviewer

  • Deployments need the internal schema applied (spky migrate / redeploy) before the tab does anything; until then getFlags returns isAdmin: false for everyone and the tab says so explicitly. Targets: threads, whitepawn.
  • Not verified by clicking through a loaded extension — the bridge round trip is covered by build + typecheck only. Everything server-side is verified against a live SurrealDB 3.1.
  • Two migrate::tests fail on this branch, but they also fail on a clean main — pre-existing and unrelated.
  • Two pre-existing issues found and left alone: the fn::job::kill/retry comment claiming they are root-only (they default to FULL), and the scheduler sweep's skip-if-exists letting a mid-tick signup stick on a stale variant.

🤖 Generated with Claude Code

Flags could only be changed with `spky flag` from a terminal holding root
credentials. This adds a Flags tab to the DevTools extension with two
capabilities that have deliberately different blast radii:

- Local overrides force a variant in one browser. No auth, no network,
  works signed out and offline. Persisted to page-origin localStorage and
  exposed as client.setFeatureOverride/clearFeatureOverrides.
- Remote changes flip a flag's `enabled` bit or edit an allowlist for every
  user, and require admin rights.

sp00ky had no admin concept, only record tokens and root. The `user` table
belongs to the app, so gating on a `role` field would both require every app
to adopt one and hand users a field they could write. Instead `_00_admin` is
a new internal table, root-written via `spky admin add|remove|list`, denying
create/update/delete unconditionally so nobody self-promotes. Its select rule
is self-scoped, so an admin confirms their own status without the roster
being enumerable.

Per-user assignments are derived, so writing a definition syncs nothing on
its own. `fn::feature::materialize` re-evaluates one flag for all users into
the already-live-synced `_00_user_feature`; `allow`/`disallow` edit the
allowlist and re-materialize.

Three SurrealDB behaviours shaped this, verified against surrealdb-core 3.1.5
rather than assumed:

- Permission-clause subqueries run with checks disabled, so `_00_admin` can
  gate other tables while staying invisible itself.
- DEFINE FUNCTION defaults to PERMISSIONS FULL. Omitting the clause would
  ship a self-service flag editor to every signed-in user, so all three
  mutations carry an explicit clause and a unit test asserts it.
- Custom functions are not security-definer; the body runs as the caller.
  That is why `_00_user_feature` opens writes to admins, and why the
  `SELECT id FROM user` inside materialize is permission-filtered. The
  scheduler now stamps the root-enumerated user count so materialize throws
  loudly instead of silently updating only the admin.

Also fixes fn::feature::hash, which was broken on SurrealDB 3: the int cast
no longer accepts hex and hex literals were removed from the language, so
`<int>'0x5f7bdee4'` errors. Nothing called it before (both Rust evaluators
hash themselves), but every rollout rule would have failed once materialize
did. Replaced with a hand-rolled hex fold that reproduces the pinned Rust
golden vectors.

allow/disallow are read-modify-write over the rules array, matching
`spky flag`. Concurrent admins collide, but SurrealDB fails the loser with a
retryable transaction conflict rather than dropping the write, so the bridge
retries with jitter instead of surfacing an engine error.

Tests: new Docker-backed e2e asserts the SurrealQL evaluator matches the Rust
golden vectors and the full permission matrix (non-admins cannot read
definitions, forge assignments, or join the roster); 10 new unit tests for
overrides; the Playwright permission spec gains admin coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://mono424.github.io/sp00ky/pr-preview/pr-36/

Built to branch gh-pages at 2026-08-07 21:54 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant